`timescale 1ns / 1ps

module pwc2_buffer
#(
    parameter N_INH     = 4,    // N_IN HEIGHT, must be 1, 2, 4, 8, ...
    parameter N_INW     = 4,    // N_IN WIDTH
    parameter N_OUT     = 16,
    parameter N_ICH     = 384,
    parameter N_OCH     = 512,
    parameter BIT       = 8,
    parameter RAM_TYPE  = "distributed",
    parameter RAM_LATENCY = 1
)
(
    input   clk,
    input   rst,
    
    input   i_vld,
    output  i_rdy,
    input   [N_INW*BIT-1 : 0]    i_data,
    
    output  o_vld,
    input   o_rdy,
    output  o_last,
    output  [N_INH*N_INW*BIT-1 : 0]    o_data
);

localparam FOLD_I = N_ICH / N_INW;
localparam FOLD_O = N_OCH / N_OUT;

localparam WDEPTH = 2 * FOLD_I * N_INH;
localparam WDATA_WIDTH = N_INW * BIT;
localparam WADDR_WIDTH = $clog2(WDEPTH);

localparam RDEPTH = WDEPTH / N_INH;
localparam RDATA_WIDTH = WDATA_WIDTH * N_INH;
localparam RADDR_WIDTH = $clog2(RDEPTH);

wire w_en;
wire [WADDR_WIDTH-1 : 0] w_addr;
wire [WDATA_WIDTH-1 : 0] w_data;
wire r_en;
wire [RADDR_WIDTH-1 : 0] r_addr;
wire [RDATA_WIDTH-1 : 0] r_data;

reg  [1 : 0] buffer_state;
wire [1 : 0] buffer_clr;
wire [1 : 0] buffer_set;

always @(posedge clk)
begin
    if (rst)
        buffer_state <= 2'b0;
    else
    begin
        if (buffer_clr[0])
            buffer_state[0] <= 1'b0;
        else if (buffer_set[0])
            buffer_state[0] <= 1'b1;
        
        if (buffer_clr[1])
            buffer_state[1] <= 1'b0;
        else if (buffer_set[1])
            buffer_state[1] <= 1'b1;
    end
end

pwc2_buffer_writer #(
    .N_INH     (N_INH),
    .FOLD_I    (FOLD_I),
    .D_WIDTH   (WDATA_WIDTH),
    .A_WIDTH   (WADDR_WIDTH)
) inst_writer
(
    .clk            (clk),
    .rst            (rst),
    
    .i_vld          (i_vld),
    .i_rdy          (i_rdy),
    .i_data         (i_data),

    .bstate         (buffer_state),
    .bstate_set     (buffer_set),
    
    .w_en           (w_en),
    .w_addr         (w_addr),
    .w_data         (w_data)
);

pwc2_buffer_reader #(
    .FOLD_I         (FOLD_I),
    .FOLD_O         (FOLD_O),
    .D_WIDTH        (RDATA_WIDTH),
    .A_WIDTH        (RADDR_WIDTH),
    .RAM_LATENCY    (RAM_LATENCY)
) inst_reader
(
    .clk            (clk),
    .rst            (rst),
    
    .o_vld          (o_vld),
    .o_rdy          (o_rdy),
    .o_last         (o_last),
    .o_data         (o_data),

    .bstate         (buffer_state),
    .bstate_clr     (buffer_clr),
    
    .r_en           (r_en),
    .r_addr         (r_addr),
    .r_data         (r_data)
);

generate
    if (N_INH == 1)
    begin
        zq_sdpram #(
            .ADDR_WIDTH ( WADDR_WIDTH ),
            .DATA_WIDTH ( WDATA_WIDTH ),
            .DEPTH      (      WDEPTH ),
            .LATENCY    ( RAM_LATENCY ),
            .RAMTYPE    ( RAM_TYPE    )
        )
        inst_sdpram (
            .clk                     ( clk       ),

            .ena_w                   ( w_en      ),
            .wea                     ( 1'b1      ),
            .addr_w                  ( w_addr    ),
            .din                     ( w_data    ),

            .ena_r                   ( r_en      ),
            .addr_r                  ( r_addr    ),
            .dout                    ( r_data    )
        );
    end
    else
    begin
        if (RAM_TYPE == "block") begin
            zq_sdpram_asym_blk #(
                .WADDR_WIDTH ( WADDR_WIDTH ),
                .WDATA_WIDTH ( WDATA_WIDTH ),
                .WDEPTH      (      WDEPTH ),
                .RATIO       (      N_INH  ),
                .LATENCY     ( RAM_LATENCY ),
                .RAMTYPE     ( RAM_TYPE    )
            )
            inst_sdpram_asym (
                .clk                     ( clk       ),

                .ena_w                   ( w_en ),
                .addr_w                  ( w_addr    ),
                .din                     ( w_data    ),

                .ena_r                   ( r_en      ),
                .addr_r                  ( r_addr    ),
                .dout                    ( r_data    )
            );
        end
        else begin
            zq_sdpram_asym_lut #(
                .WADDR_WIDTH ( WADDR_WIDTH ),
                .WDATA_WIDTH ( WDATA_WIDTH ),
                .WDEPTH      (      WDEPTH ),
                .RATIO       (      N_INH  ),
                .LATENCY     ( RAM_LATENCY ),
                .RAMTYPE     ( RAM_TYPE    )
            )
            inst_sdpram_asym (
                .clk                     ( clk       ),

                .ena_w                   ( w_en ),
                .addr_w                  ( w_addr    ),
                .din                     ( w_data    ),

                .ena_r                   ( r_en      ),
                .addr_r                  ( r_addr    ),
                .dout                    ( r_data    )
            );
        end
    end
endgenerate

endmodule


module pwc2_buffer_writer
#(
    parameter N_INH     = 4,
    parameter FOLD_I    = 384 / 4,
    parameter D_WIDTH   = 16 * 8,
    parameter A_WIDTH   = 6
)
(
    input   clk,
    input   rst,
    
    input   i_vld,
    output  i_rdy,
    input   [D_WIDTH-1 : 0]     i_data,

    input   [1 : 0]             bstate,
    output  [1 : 0]             bstate_set,
    
    output  w_en,
    output  [A_WIDTH-1 : 0]     w_addr,
    output  [D_WIDTH-1 : 0]     w_data
);

localparam S_WT_0 = 2'b01;
localparam S_WT_1 = 2'b10;
localparam ALO_WIDTH = $clog2(N_INH);
localparam AHI_WIDTH = A_WIDTH - ALO_WIDTH;
localparam [AHI_WIDTH-1: 0] ZEROS = {AHI_WIDTH{1'b0}};
localparam [AHI_WIDTH-1: 0] ADDR_MID = FOLD_I;
localparam [AHI_WIDTH-1: 0] ADDR_MAX_0 = 1 * FOLD_I - 1;
localparam [AHI_WIDTH-1: 0] ADDR_MAX_1 = 2 * FOLD_I - 1;

reg  [1 : 0] cstate;
reg  [AHI_WIDTH-1 : 0] addr_hi;

reg  [1 : 0] nstate;
reg  [1 : 0] bs_set;
reg  enable;
reg  ii_rdy;
wire fh_last;
wire [1 : 0] fi_last;
wire [1 : 0] fih_lst;

assign i_rdy = ii_rdy;
assign bstate_set = bs_set;
assign w_en = enable;
assign w_data = i_data;
assign fi_last[0] = (addr_hi == ADDR_MAX_0);
assign fi_last[1] = (addr_hi == ADDR_MAX_1);
assign fih_lst[0] = fi_last[0] & fh_last;
assign fih_lst[1] = fi_last[1] & fh_last;

always @(posedge clk)
begin
    if (rst)
        addr_hi <= ZEROS;
    else
    begin
        if (enable)
        begin
            if (fi_last[0])
                addr_hi <= fh_last ? ADDR_MID : ZEROS;
            else if (fi_last[1])
                addr_hi <= fh_last ? ZEROS : ADDR_MID;
            else
                addr_hi <= addr_hi + 1'b1;
        end
    end
end

generate
    if (N_INH != 1)
    begin
        wire [ALO_WIDTH-1 : 0] addr_lo;
        wire fh_en;

        assign w_addr = { addr_hi, addr_lo };
        assign fh_en = enable & (fi_last[0] | fi_last[1]);

        zq_counter #(
            .N  (N_INH)
        ) inst_fo
        (
            .clk    (clk),
            .rst    (rst),
            .clken  (fh_en),
            .last   (fh_last),
            .out    (addr_lo)
        );
    end
    else
    begin
        assign w_addr = addr_hi;
        assign fh_last = 1'b1;
    end
endgenerate

always @(posedge clk)
begin
    if (rst)
        cstate <= S_WT_0;
    else
        cstate <= nstate;
end

always @(*)
begin
    (* full_case *)
    case (cstate)
		S_WT_0:
        begin
            if (~bstate[0] & i_vld & fih_lst[0])
                nstate = S_WT_1;
            else
                nstate = S_WT_0;
		end
		S_WT_1:
        begin
            if (~bstate[1] & i_vld & fih_lst[1])
                nstate = S_WT_0;
            else
                nstate = S_WT_1;
		end
		default:
        begin
			nstate = S_WT_0;
		end
	endcase
end

always @(*)
begin
    (* full_case *)
    case (cstate)
		S_WT_0:
        begin
            ii_rdy = ~bstate[0];
            enable = ~bstate[0] & i_vld;
            bs_set[0] = ~bstate[0] & i_vld & fih_lst[0];
            bs_set[1] = 1'b0;
		end
		S_WT_1:
        begin
            ii_rdy = ~bstate[1];
            enable = ~bstate[1] & i_vld;
            bs_set[0] = 1'b0;
            bs_set[1] = ~bstate[1] & i_vld & fih_lst[1];
		end
	endcase
end

endmodule


module pwc2_buffer_reader
#(
    parameter FOLD_I    = 384 / 16,
    parameter FOLD_O    = 512 / 16,
    parameter D_WIDTH   = 16 * 8,
    parameter A_WIDTH   = 6,
    parameter RAM_LATENCY = 1
)
(
    input   clk,
    input   rst,
    
    input   o_rdy,
    output  o_vld,
    output  o_last,
    output  [D_WIDTH-1 : 0]     o_data,

    input   [1 : 0]             bstate,
    output  [1 : 0]             bstate_clr,
    
    output  r_en,
    output  [A_WIDTH-1 : 0]     r_addr,
    input   [D_WIDTH-1 : 0]     r_data
);

localparam S_RD_0 = 2'b01;
localparam S_RD_1 = 2'b10;
localparam [A_WIDTH-1: 0] ZEROS = {A_WIDTH{1'b0}};
localparam [A_WIDTH-1: 0] ADDR_MID = FOLD_I; 
localparam [A_WIDTH-1: 0] ADDR_MAX_0 = 1 * FOLD_I - 1;
localparam [A_WIDTH-1: 0] ADDR_MAX_1 = 2 * FOLD_I - 1;

reg  [1 : 0] cstate;
reg  [A_WIDTH-1 : 0] c_addr;

reg  [1 : 0] nstate;
reg  [1 : 0] bs_clr;
reg  enable;
reg  oo_vld;
reg  oo_last;

wire         oo_rdy;
wire [1 : 0] fi_last;
wire         fo_last;
wire         fo_en;
wire [1 : 0] fio_lst;

assign bstate_clr = bs_clr;
assign r_en = oo_rdy;
assign r_addr = c_addr;
assign o_data = r_data;

assign oo_rdy = ~o_vld | o_rdy;
assign fi_last[0] = c_addr == ADDR_MAX_0;
assign fi_last[1] = c_addr == ADDR_MAX_1;
assign fo_en = enable & (fi_last[0] | fi_last[1]);
assign fio_lst[0] = fi_last[0] & fo_last;
assign fio_lst[1] = fi_last[1] & fo_last;

always @(posedge clk)
begin
    if (rst)
        c_addr <= ZEROS;
    else
    begin
        if (enable)
        begin
            if (fi_last[0])
                c_addr <= fo_last ? ADDR_MID : ZEROS;
            else if (fi_last[1])
                c_addr <= fo_last ? ZEROS : ADDR_MID;
            else
                c_addr <= c_addr + 1'b1;
        end
    end
end

zq_counter #(
    .N  (FOLD_O)
) inst_fo
(
    .clk    (clk),
    .rst    (rst),
    .clken  (fo_en),
    .last   (fo_last),
    .out    ()
);

shift_reg_rst #(
    .N          (RAM_LATENCY),
    .WIDTH      (2)
) inst_vld_reg
(
    .clk        (clk),
    .rst        (rst),
    .clken      (oo_rdy),
    .i_data     ({oo_vld, oo_last}),
    .o_data     ({ o_vld,  o_last})
);

always @(posedge clk)
begin
    if (rst)
        cstate <= S_RD_0;
    else
        cstate <= nstate;
end

always @(*)
begin
    (* full_case *)
    case (cstate)
		S_RD_0:
        begin
            if (bstate[0] & oo_rdy & fio_lst[0])
                nstate = S_RD_1;
            else
                nstate = S_RD_0;
		end
		S_RD_1:
        begin
            if (bstate[1] & oo_rdy & fio_lst[1])
                nstate = S_RD_0;
            else
                nstate = S_RD_1;
		end
		default:
        begin
			nstate = S_RD_0;
		end
	endcase
end

always @(*)
begin
    (* full_case *)
    case (cstate)
		S_RD_0:
        begin
            oo_vld = bstate[0];
            enable = bstate[0] & oo_rdy;
            oo_last = bstate[0] & fi_last[0];
            bs_clr[0] = bstate[0] & oo_rdy & fio_lst[0];
            bs_clr[1] = 1'b0;
		end
		S_RD_1:
        begin
            oo_vld = bstate[1];
            enable = bstate[1] & oo_rdy;
            oo_last = bstate[1] & fi_last[1];
            bs_clr[0] = 1'b0;
            bs_clr[1] = bstate[1] & oo_rdy & fio_lst[1];
		end
	endcase
end

endmodule
